<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category  Zend
 * @package   Zend_Navigation
 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Container.php 19179 2009-11-22 16:12:57Z dragonbe $
 */

/**
 * Zend_Navigation_Container
 *
 * Container class for Zend_Navigation_Page classes.
 *
 * @category  Zend
 * @package   Zend_Navigation
 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Navigation_Container implements RecursiveIterator, Countable
{
	/**
	 * Contains sub pages
	 *
	 * @var array
	 */
	protected $_pages = array();

	/**
	 * An index that contains the order in which to iterate pages
	 *
	 * @var array
	 */
	protected $_index = array();

	/**
	 * Whether index is dirty and needs to be re-arranged
	 *
	 * @var bool
	 */
	protected $_dirtyIndex = false;

	// Internal methods:

	/**
	 * Sorts the page index according to page order
	 *
	 * @return void
	 */
	protected function _sort()
	{
		if ($this->_dirtyIndex) {
			$newIndex = array();
			$index = 0;

			foreach ($this->_pages as $hash => $page) {
				$order = $page->getOrder();
				if ($order === null) {
					$newIndex[$hash] = $index;
					$index++;
				} else {
					$newIndex[$hash] = $order;
				}
			}

			asort($newIndex);
			$this->_index = $newIndex;
			$this->_dirtyIndex = false;
		}
	}

	// Public methods:

	/**
	 * Notifies container that the order of pages are updated
	 *
	 * @return void
	 */
	public function notifyOrderUpdated()
	{
		$this->_dirtyIndex = true;
	}

	/**
	 * Adds a page to the container
	 *
	 * This method will inject the container as the given page's parent by
	 * calling {@link Zend_Navigation_Page::setParent()}.
	 *
	 * @param  Zend_Navigation_Page|array|Zend_Config $page  page to add
	 * @return Zend_Navigation_Container                     fluent interface,
	 *                                                       returns self
	 * @throws Zend_Navigation_Exception                     if page is invalid
	 */
	public function addPage($page)
	{
		if ($page === $this) {
			require_once 'Zend/Navigation/Exception.php';
			throw new Zend_Navigation_Exception(
                'A page cannot have itself as a parent');
		}

		if (is_array($page) || $page instanceof Zend_Config) {
			require_once 'Zend/Navigation/Page.php';
			$page = Zend_Navigation_Page::factory($page);
		} elseif (!$page instanceof Zend_Navigation_Page) {
			require_once 'Zend/Navigation/Exception.php';
			throw new Zend_Navigation_Exception(
                    'Invalid argument: $page must be an instance of ' .
                    'Zend_Navigation_Page or Zend_Config, or an array');
		}

		$hash = $page->hashCode();

		if (array_key_exists($hash, $this->_index)) {
			// page is already in container
			return $this;
		}

		// adds page to container and sets dirty flag
		$this->_pages[$hash] = $page;
		$this->_index[$hash] = $page->getOrder();
		$this->_dirtyIndex = true;

		// inject self as page parent
		$page->setParent($this);

		return $this;
	}

	/**
	 * Adds several pages at once
	 *
	 * @param  array|Zend_Config $pages   pages to add
	 * @return Zend_Navigation_Container  fluent interface, returns self
	 * @throws Zend_Navigation_Exception  if $pages is not array or Zend_Config
	 */
	public function addPages($pages)
	{
		if ($pages instanceof Zend_Config) {
			$pages = $pages->toArray();
		}

		if (!is_array($pages)) {
			require_once 'Zend/Navigation/Exception.php';
			throw new Zend_Navigation_Exception(
                    'Invalid argument: $pages must be an array or an ' .
                    'instance of Zend_Config');
		}

		foreach ($pages as $page) {
			$this->addPage($page);
		}

		return $this;
	}

	/**
	 * Sets pages this container should have, removing existing pages
	 *
	 * @param  array $pages               pages to set
	 * @return Zend_Navigation_Container  fluent interface, returns self
	 */
	public function setPages(array $pages)
	{
		$this->removePages();
		return $this->addPages($pages);
	}

	/**
	 * Returns pages in the container
	 *
	 * @return array  array of Zend_Navigation_Page instances
	 */
	public function getPages()
	{
		return $this->_pages;
	}

	/**
	 * Removes the given page from the container
	 *
	 * @param  Zend_Navigation_Page|int $page  page to remove, either a page
	 *                                         instance or a specific page order
	 * @return bool                            whether the removal was
	 *                                         successful
	 */
	public function removePage($page)
	{
		if ($page instanceof Zend_Navigation_Page) {
			$hash = $page->hashCode();
		} elseif (is_int($page)) {
			$this->_sort();
			if (!$hash = array_search($page, $this->_index)) {
				return false;
			}
		} else {
			return false;
		}

		if (isset($this->_pages[$hash])) {
			unset($this->_pages[$hash]);
			unset($this->_index[$hash]);
			$this->_dirtyIndex = true;
			return true;
		}

		return false;
	}

	/**
	 * Removes all pages in container
	 *
	 * @return Zend_Navigation_Container  fluent interface, returns self
	 */
	public function removePages()
	{
		$this->_pages = array();
		$this->_index = array();
		return $this;
	}

	/**
	 * Checks if the container has the given page
	 *
	 * @param  Zend_Navigation_Page $page       page to look for
	 * @param  bool                 $recursive  [optional] whether to search
	 *                                          recursively. Default is false.
	 * @return bool                             whether page is in container
	 */
	public function hasPage(Zend_Navigation_Page $page, $recursive = false)
	{
		if (array_key_exists($page->hashCode(), $this->_index)) {
			return true;
		} elseif ($recursive) {
			foreach ($this->_pages as $childPage) {
				if ($childPage->hasPage($page, true)) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Returns true if container contains any pages
	 *
	 * @return bool  whether container has any pages
	 */
	public function hasPages()
	{
		return count($this->_index) > 0;
	}

	/**
	 * Returns a child page matching $property == $value, or null if not found
	 *
	 * @param  string $property           name of property to match against
	 * @param  mixed  $value              value to match property against
	 * @return Zend_Navigation_Page|null  matching page or null
	 */
	public function findOneBy($property, $value)
	{
		$iterator = new RecursiveIteratorIterator($this,
		RecursiveIteratorIterator::SELF_FIRST);

		foreach ($iterator as $page) {
			if ($page->get($property) == $value) {
				return $page;
			}
		}

		return null;
	}

	/**
	 * Returns all child pages matching $property == $value, or an empty array
	 * if no pages are found
	 *
	 * @param  string $property  name of property to match against
	 * @param  mixed  $value     value to match property against
	 * @return array             array containing only Zend_Navigation_Page
	 *                           instances
	 */
	public function findAllBy($property, $value)
	{
		$found = array();

		$iterator = new RecursiveIteratorIterator($this,
		RecursiveIteratorIterator::SELF_FIRST);

		foreach ($iterator as $page) {
			if ($page->get($property) == $value) {
				$found[] = $page;
			}
		}

		return $found;
	}

	/**
	 * Returns page(s) matching $property == $value
	 *
	 * @param  string $property  name of property to match against
	 * @param  mixed  $value     value to match property against
	 * @param  bool   $all       [optional] whether an array of all matching
	 *                           pages should be returned, or only the first.
	 *                           If true, an array will be returned, even if not
	 *                           matching pages are found. If false, null will
	 *                           be returned if no matching page is found.
	 *                           Default is false.
	 * @return Zend_Navigation_Page|null  matching page or null
	 */
	public function findBy($property, $value, $all = false)
	{
		if ($all) {
			return $this->findAllBy($property, $value);
		} else {
			return $this->findOneBy($property, $value);
		}
	}

	/**
	 * Magic overload: Proxy calls to finder methods
	 *
	 * Examples of finder calls:
	 * <code>
	 * // METHOD                    // SAME AS
	 * $nav->findByLabel('foo');    // $nav->findOneBy('label', 'foo');
	 * $nav->findOneByLabel('foo'); // $nav->findOneBy('label', 'foo');
	 * $nav->findAllByClass('foo'); // $nav->findAllBy('class', 'foo');
	 * </code>
	 *
	 * @param  string $method             method name
	 * @param  array  $arguments          method arguments
	 * @throws Zend_Navigation_Exception  if method does not exist
	 */
	public function __call($method, $arguments)
	{
		if (@preg_match('/(find(?:One|All)?By)(.+)/', $method, $match)) {
			return $this->{$match[1]}($match[2], $arguments[0]);
		}

		require_once 'Zend/Navigation/Exception.php';
		throw new Zend_Navigation_Exception(sprintf(
                'Bad method call: Unknown method %s::%s',
		get_class($this),
		$method));
	}

	/**
	 * Returns an array representation of all pages in container
	 *
	 * @return array
	 */
	public function toArray()
	{
		$pages = array();

		$this->_dirtyIndex = true;
		$this->_sort();
		$indexes = array_keys($this->_index);
		foreach ($indexes as $hash) {
			$pages[] = $this->_pages[$hash]->toArray();
		}
		return $pages;
	}

	// RecursiveIterator interface:

	/**
	 * Returns current page
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return Zend_Navigation_Page       current page or null
	 * @throws Zend_Navigation_Exception  if the index is invalid
	 */
	public function current()
	{
		$this->_sort();
		current($this->_index);
		$hash = key($this->_index);

		if (isset($this->_pages[$hash])) {
			return $this->_pages[$hash];
		} else {
			require_once 'Zend/Navigation/Exception.php';
			throw new Zend_Navigation_Exception(
                    'Corruption detected in container; ' .
                    'invalid key found in internal iterator');
		}
	}

	/**
	 * Returns hash code of current page
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return string  hash code of current page
	 */
	public function key()
	{
		$this->_sort();
		return key($this->_index);
	}

	/**
	 * Moves index pointer to next page in the container
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return void
	 */
	public function next()
	{
		$this->_sort();
		next($this->_index);
	}

	/**
	 * Sets index pointer to first page in the container
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return void
	 */
	public function rewind()
	{
		$this->_sort();
		reset($this->_index);
	}

	/**
	 * Checks if container index is valid
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return bool
	 */
	public function valid()
	{
		$this->_sort();
		return current($this->_index) !== false;
	}

	/**
	 * Proxy to hasPages()
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return bool  whether container has any pages
	 */
	public function hasChildren()
	{
		return $this->hasPages();
	}

	/**
	 * Returns the child container.
	 *
	 * Implements RecursiveIterator interface.
	 *
	 * @return Zend_Navigation_Page|null
	 */
	public function getChildren()
	{
		$hash = key($this->_index);

		if (isset($this->_pages[$hash])) {
			return $this->_pages[$hash];
		}

		return null;
	}

	// Countable interface:

	/**
	 * Returns number of pages in container
	 *
	 * Implements Countable interface.
	 *
	 * @return int  number of pages in the container
	 */
	public function count()
	{
		return count($this->_index);
	}
}
